﻿//----------------------------------------------------------------------------
// A Simple flying oval simulator, using Units of Measure
//----------------------------------------------------------------------------

module MoonRace.Main

open System
open System.Windows.Forms
open System.Drawing
open Microsoft.FSharp.Control.CommonExtensions

open MoonRace.Physics

//-----------------------------------------------
// Graphics System

[<Measure>]
type pixels

// Define a special type of form that doesn't flicker
type SmoothForm() as x = 
    inherit Form()
    do x.DoubleBuffered <- true

let mutable LastMousePosition = 0.0<pixels>,0.0<pixels>
        
let form = new SmoothForm(Text="F# Awesome dot game",  Visible=true, TopMost=false, Width=1200, Height=700)
form.MouseMove.AddHandler(
    new MouseEventHandler(
        fun sender clickArgs -> 
            LastMousePosition <- (float clickArgs.X)*1.0<pixels>, (float clickArgs.Y)*1.0<pixels>
    )
)

type IPaintObject = 
    abstract Paint : Graphics -> unit
    
// Keep a list of objects to draw
let mutable paintObjects = new ResizeArray<IPaintObject>()

form.Paint.Add (fun args -> 

        let g = args.Graphics
        // Clear the form
        g.Clear(color=Color.Blue)

        // Draw the paint objects
        for paintObject in paintObjects do
           paintObject.Paint(g)

        // Invalidate the form again in 10 milliseconds to get continuous update
        async { do! Async.Sleep(10)
                form.Invalidate() } |> Async.Start
   )

// Set things going with an initial Invaldiate
form.Invalidate()

[<Measure>]
type sRealTime

type System.TimeSpan with 
    member x.TotalSecondsTyped = (x.TotalSeconds * 1.0<sRealTime>)

let Pixels_per_m = 200.0<pixels/m>
let m_per_pixel = 1.0/Pixels_per_m

// Make 5 seconds into one year
let sec_per_year = 60.0<s> * 60.0 * 24.0 * 365.0

// One second of real time is 1 second of model time. Too simple
let realTimeToModelTime (x:float<sRealTime>) = 1.0<s>

let pixelPointToModelPoint (x,y) = m_per_pixel * x, m_per_pixel * y

let pixels (x:float<pixels>) = int32 x


//-----------------------------------------------
// The guy!

type Man(x:float<m>, y:float<m>, drawFunction) =

    member this.X = x
    member this.Y = y

    member this.move (vx:float<m/s>,vy:float<m/s>) (time:float<s>) = 
        new Man(x + time*vx, y + time*vy, drawFunction)


    interface IPaintObject with
              member obj.Paint(g) = drawFunction g (x,y)








//-----------------------------------------------
// Asynchronous simulation thread

type Simulator() = 
    // Get the start time for the animation
    let startTime = System.DateTime.Now
    let lastTimeOption = ref None

    let targetPoint = ref (pixelPointToModelPoint LastMousePosition)

    let ComputeVelocity (obj:Man) (tx,ty) = 
        let dx = (tx-obj.X)
        let dy = (ty-obj.Y)
        let d2 = (dx*dx) + (dy*dy)
        let d = sqrt d2
        let vx = (dx / d) * 0.1<1/s> * d   // the 0.1 is very arbitrary
        let vy = (dy / d) * 0.1<1/s> * d
        vx,vy

    let stepMan man timeDiff target = 
        let (vx,vy) = ComputeVelocity man target
        man.move (vx,vy) timeDiff            

    let fakeMan = new Man(-1000.0<m>, -1000.0<m>, (fun g (x,y) -> ()))

    member sim.Step(time:TimeSpan, targetpoint) = 
        match !lastTimeOption with
        | None -> ()
        | Some(lastTime) -> 

              // go through each paintobject and update it if possible
              let updatedGuys = (paintObjects.ToArray()) 
                                |> Array.map (fun(obj:IPaintObject) -> 
                                    let timeStep = (time - lastTime).TotalSecondsTyped |>  realTimeToModelTime
                                    match obj with
                                    | :? Man as obj ->                                                                            
                                        (stepMan obj timeStep !targetPoint) :> IPaintObject
                                    | _ -> failwith "Don't know how to simulate one of these") 

              paintObjects <- new ResizeArray<IPaintObject>(updatedGuys)

        lastTimeOption := Some time 
           
           

    member sim.Start() = 
        async { while true do 
                   let time = System.DateTime.Now - startTime
                   // Sleep a little to give better GUI updates
                   do! Async.Sleep(1) 
                   targetPoint := pixelPointToModelPoint LastMousePosition
                   sim.Step(time, !targetPoint)
              } 
        |> Async.Start

let s = Simulator().Start() 







let drawAStandardGuy (g:Graphics) (x,y) = 
    let width = 20
    let height = 50
    let brush = Brushes.Pink
    let rect = Rectangle(x=pixels (x * Pixels_per_m)-width/2,
                         y=pixels (y * Pixels_per_m)-height/2,
                         width=width, 
                         height=height)               
    g.FillEllipse(brush,rect)

    

let guy   = new Man(x = 1.1<m>, y = 1.1<m>, drawFunction = drawAStandardGuy)


paintObjects.Add(guy)

form.Show()

#if COMPILED
[<STAThread>]
do Application.Run(form)
#endif
